package algo_lignes_sites.data.pre_trt.pre_trt_path;

import algo_lignes_sites.data.junction.A_Junction;
import algo_lignes_sites.data.junction.JunctionREF;
import algo_lignes_sites.data.path.A_Path;
import algo_lignes_sites.data.path.PathREF;
import algo_lignes_sites.params.Params_ls;
import algo_lignes_sites.params.objects.contraintesgeom.ContrainteGeomLine;
import commons.data.EchelleGeneralization;
import commons.logs.Logger;
import commons.params_appli.ParamsAppli;
import commons.spatialindex.simplegrid.SimpleGridSPI;
import commons.utils.Utils.Side;
import commons.utils.primitive_geom.Point_d;
import commons.utils.primitive_geom.Vector_d;
import java.util.ArrayList;

public class DissymetrieOptimizer {

	private static ArrayList<PathREF> vAllPathREF_withDissymetrie;
	private static SimpleGridSPI spiAllPath;



	static public void optimizeDissymetrie(ArrayList<PathREF> vAllPathREF, Params_ls params, SimpleGridSPI spiPath) {
		Logger.log_INFO1("-- Optimisation des dissymétries --");

		if (!params.bOptimizeDissymetrie) {
			Logger.log_INFO2("  -> fonction désactivée dans les paramètres");
			return;
		}

		DissymetrieOptimizer.vAllPathREF_withDissymetrie = getPathWithDissymetrieAndNotYetOriented(vAllPathREF);
		
		// SPI pour la recherche d'intersection entre barbulles d'un path et les autres path
		DissymetrieOptimizer.spiAllPath = spiPath;
		spiAllPath.clear();
		spiAllPath.addObjects(vAllPathREF);

		Logger.log_INFO2(" - nb Total de Path : " + vAllPathREF.size());
		Logger.log_INFO2(" - nb Path avec dissymetrie : " + vAllPathREF_withDissymetrie.size());


		// Tout d'abord, détection des contraintes d'orientation
		double factor_lgMaxEntre2Barbules = ParamsAppli.getParam_d("LigneSite.FACTEUR_LG_MAX_ENTRE_2_BARBULES");
		detecteContrainteOrientation(factor_lgMaxEntre2Barbules);


		// Arrivé là, on doit faire attention : les path qui n'ont pas de contraintes d'orientation
		//  (car tres peu de barbules en intersection / nb total de barbule)
		//  doivent être "adaptés" rapport aux éventueles path connecté qui eux sont "contraints"
		//  ceci afin d'éviter des changement de sens le long de path connectés de bout en bout

		// La stratégie d'orientation est :
		// - 1ere passe sur les path "contraint" : on force leur orientation selon les intersections de leur barbules droite et gauche
		// - à chaque fois, on poursuit recursivement sur les path voisin, s'ils sont adaptables, on les oriente de la meme façon
		// - après, il reste un certain nombre de path adaptables mais non reliés (via des CONNECT_2) a des path contraint
		//

		// - puis parcours successif des path "adaptables" en plusieurs passes
		//    pour chaque passe, on adapte un path s'il est connecté a un path dont l'orientation a déjà été établie
		//    (c'est a dire aux path contraints pour la 1ere passe, puis autre connectés, successivement, aux passes ultérieures)
		// - a la fin il reste peut etre encore des path non orientés
		orienteAllPath();

		clear();
	}

	// Détruit les objets éventuellement créés par ce module et supprime les liens statiques avec les objets passés en paramètres
	private static void clear() {
		if (vAllPathREF_withDissymetrie!=null)
			vAllPathREF_withDissymetrie.clear();
		vAllPathREF_withDissymetrie = null;
		spiAllPath = null;
	}

	//**************************************************************************
	//  Detection des contraintes d'orientation
	//**************************************************************************

	private static void detecteContrainteOrientation(double factor_lgMaxEntre2Barbules) {

		for (PathREF path : vAllPathREF_withDissymetrie)
			detectionContrainteOrientation(path, factor_lgMaxEntre2Barbules);

		// petite stat en fin de traitement
		int nbPathContraint = 0;
		for (PathREF path : vAllPathREF_withDissymetrie)
			if (hasOrientationContrainte(path)) nbPathContraint++;
		Logger.log_INFO2(" - nb Path avec contrainte d'orientation : " + nbPathContraint);
	}


	private static void detectionContrainteOrientation(PathREF path, double factor_lgMaxEntre2Barbules) {
		path.nbTotal_barbule = 0;
		path.nbIntersect_barbuleDroite = 0;
		path.nbIntersect_barbuleGauche = 0;

		// les vérifications qui suivent ont été lancée avant la simplification des path
		//  donc on ne gère pas l'erreur proprement
		assert (path.getEchelleGeneralization()!=null);
		assert (path.getContrainteGeom()!=null);

		ContrainteGeomLine cg = path.getContrainteGeom();
		EchelleGeneralization eg = path.getEchelleGeneralization();

		double echelleCiblePath_inv = eg.echelle_inv;

		// Lg max au dela de laquelle un segment est trop long et doit etre subdivisé pour la création des barbulles ?
		// dépendent de sa largeur (et donc de l'échelle etc.)
		double lgMax = factor_lgMaxEntre2Barbules * cg.epaisseurTrait_papier_mm / 1000.0 * echelleCiblePath_inv;

		// 1/2 (largeur_principale + largeur_dissymetrie) + espace autour
		// => cela donnera le module du vecteur normal de test d'intersection
		// TODO_KO on devrait ajouter la 1/2 largeur des path avec lesquels on va calculer l'intersection, mais on ne connait pas la largeur a priori !
		//  -> pour faire simple, on ajoute la meme 1/2 largeur que le path en cours, c'est mieux que rien ...
		double moduleVectNormalTestIntersect = (cg.espaceAutour_papier_mm + 0.5*(cg.epaisseurTrait_papier_mm*2 + cg.epaisseur_dissymetrie_mm)) / 1000.0 * echelleCiblePath_inv;

		ArrayList<JunctionREF> vPoint = path.getVJunctionREF();
		int nbPt = vPoint.size();

		Vector_d seg = new Vector_d();
		for (int i=0; i<nbPt-1 ; i++) {

			Point_d pt1 = vPoint.get(i);
			Point_d pt2 = vPoint.get(i+1);
			if (pt1.equalsXY(pt2)) continue;


			seg.takeCoordFrom(pt1, pt2);


			Vector_d vectNormalDroite = seg.getVectUnitaireNormalDroite().multiplyBy(moduleVectNormalTestIntersect);
			Vector_d vectNormalGauche = seg.getVectUnitaireNormalGauche().multiplyBy(moduleVectNormalTestIntersect);

			// On teste a minima les 2 extrémités du segment
			testIntersectFromThisPoint(pt1, vectNormalDroite, vectNormalGauche, path);
			testIntersectFromThisPoint(pt2, vectNormalDroite, vectNormalGauche, path);

			if (seg.getLength() > lgMax) {
				// segment trop long : on doit subdiviser pour tester d'autres barbules

				// nb de point intérieurs à tester
				int nbPtToAdd = (int) (seg.getLength()/lgMax);
//				Logger.log_DEBUG2("Test intersection barbules pour le path : " + path + " -> le segment " + i + "->" + (i+1) + " nécessite le test de " + nbPtToAdd + " barbules internes, cf. lg = " + seg.getLength() + " / " + lgMin);

				for (int j=0; j<nbPtToAdd ; j++) {
					double ratio = ((double)j+1)/(nbPtToAdd+1);
					Point_d pt = new Point_d(pt1.x + (pt2.x-pt1.x)*ratio, pt1.y + (pt2.y-pt1.y)*ratio);
					testIntersectFromThisPoint(pt, vectNormalDroite, vectNormalGauche, path);
				}
			}
		}
	}


	// Teste si la barbulle droite et gauche, a partir de pt, alignées sur les vecteur vectNormalDroite, vectNormalGauche intersecte les autres path
	// Stocke le résultat en incrémentant les nombres d'intersection adéquat du path;
	// Attention : les vecteur normaux fourni ont un from point arbitraire qu'il faut fixer à pt !
	private static void testIntersectFromThisPoint(Point_d pt, Vector_d vectNormalDroite, Vector_d vectNormalGauche, PathREF path) {
//if (path.toString().matches(".*SQUIVL31ZPOUL.*"))
//	pt=pt;
		vectNormalDroite.setPointDepart(pt);
		vectNormalGauche.setPointDepart(pt);
		boolean bIntersectDroite = hasIntersectionWithOtherPath(vectNormalDroite, path);
		boolean bIntersectGauche = hasIntersectionWithOtherPath(vectNormalGauche, path);

		path.nbTotal_barbule++;

		if (bIntersectDroite)
			path.nbIntersect_barbuleDroite++;
		if (bIntersectGauche)
			path.nbIntersect_barbuleGauche++;
	}


	// Teste si le segment 'seg' intersecte au moins un autre path que 'path'
	private static boolean hasIntersectionWithOtherPath(Vector_d seg, PathREF path) {
		ArrayList vPathProche = spiAllPath.getObjectsThatMayIntersectWith(seg);

		int nbPath = vPathProche.size();
		for (int i=0; i<nbPath ; i++) {
			
			PathREF p = (PathREF) vPathProche.get(i);
			if (p==path) continue;

			if (p.doesIntersectThisSeg(seg))
				return true;
		}
		return false;
	}



	//**************************************************************************
	//  Stratégie d'orientation
	//**************************************************************************

	// cf. commentaire de l'appel
	private static void orienteAllPath() {

		// premiere passe pour orienter les path contraints, ainsi que les path qui leur sont connectés
		for (PathREF path : vAllPathREF_withDissymetrie) {
			if (hasOrientationContrainte(path)) {
				// on force son orientation selon ses contraintes
				orientePathSelonContrainte(path);
				// puis on oriente dans le meme sens les éventuels path adaptables qui lui sont connectés, de proche en proche
				orientePathConnectedIfAdaptable_rec(path);
			}
		}

		// Arrivé la, il reste encore un certain nombre de path non encore orienté et non contraints
		ArrayList<PathREF> vPathAdaptableRestant = getPathWithDissymetrieAndNotYetOriented(vAllPathREF_withDissymetrie);
		Logger.log_INFO2(" - nb path restant a orienter apres 1ere passe : " + vPathAdaptableRestant.size());

		// Puis on fait une 2eme boucle pour orienter les path "adaptable"
		//  - on met une orientation arbitraire sur un path (mais si droite <> gauche, on choisi qd meme la meilleure orientation)
		//  - puis on propage de proche en proche sur les path connectés
		//  - puis on continue le parcours du vecteur 'vPathAdaptableRestant'
		//  (il se peut que lors de ce parcours, des path soient déjà orienté par une des propagations précédente)
		for (PathREF path : vPathAdaptableRestant) {
			if (!path.bOrientationSetted) {
				// on force son orientation selon ses contraintes "faibles"
				//  (s'il n'en a pas (droite=gauche), on fait de l'arbitraire, c'est à dire qu'on laisse l'orientation d'origine
				orientePathSelonContrainte(path);
				// puis on oriente dans le meme sens les éventuels path adaptables qui lui sont connectés, de proche en proche
				orientePathConnectedIfAdaptable_rec(path);
			}
		}

		// normalement, on a traité tous les path
		assert(getPathWithDissymetrieAndNotYetOriented(vAllPathREF_withDissymetrie).size()==0);
	}


	// A partir d'un 'path' deja orienté, parcours recursif des voisins (via CONNECT_2 uniquement)
	//  pour les orienter dans le meme sens
	private static void orientePathConnectedIfAdaptable_rec(A_Path path) {
		assert(path.bOrientationSetted == true);

		if (path.from().isConnect2() || path.from().isConnect3()) {
			A_Path path2 = getOnePathNotOrientedConnectedTo(path, path.from());
			if (path2!=null && path2.hasDissymetrie() && !hasOrientationContrainte(path2)) {
				adaptePathToPath(path2, path);
				orientePathConnectedIfAdaptable_rec(path2);
			}
		}
		if (path.to().isConnect2() || path.from().isConnect3()) {
			A_Path path2 = getOnePathNotOrientedConnectedTo(path, path.to());
			if (path2!=null && path2.hasDissymetrie() && !hasOrientationContrainte(path2)) {
				adaptePathToPath(path2, path);
				orientePathConnectedIfAdaptable_rec(path2);
			}
		}
	}


	//**************************************************************************
	// Les 2 facons d'orienter un path
	//**************************************************************************

	// 1) un path contraint s'oriente grace aux intersection détectees sur ses barbules droites et gauches
	//  si droite = gauche, on laisse l'orientation d'origine inchangée
	private static void orientePathSelonContrainte(PathREF path) {
		assert(path.bOrientationSetted == false);

		ContrainteGeomLine cg = path.getContrainteGeom();
		if ( (cg.sideDissymetrie == Side.RIGHT && path.nbIntersect_barbuleDroite>path.nbIntersect_barbuleGauche) ||
			 (cg.sideDissymetrie == Side.LEFT && path.nbIntersect_barbuleDroite<path.nbIntersect_barbuleGauche) ) {
			path.reverse();

			// inversion des stats droite / gauche
			// ce qui suit n'est que pour de l'affichage debug, pour s'y retrouver avec la droite et la gauche, comme on a inversé le path ...
			int temp = path.nbIntersect_barbuleDroite;
			path.nbIntersect_barbuleDroite = path.nbIntersect_barbuleGauche;
			path.nbIntersect_barbuleGauche = temp;
		}
		// son sort est réglé
		path.bOrientationSetted = true;
	}

	// 2) orientation d'un path adaptable selon un autre path contraint et deja orienté
	//    auquel il est connecté
	private static void adaptePathToPath(A_Path pathAdaptable, A_Path pathAlreadyOriented) {
		assert(pathAdaptable.bOrientationSetted == false);
		assert(pathAlreadyOriented.bOrientationSetted == true);
		assert(pathAdaptable.from()==pathAlreadyOriented.from() ||
			   pathAdaptable.from()==pathAlreadyOriented.to() ||
			   pathAdaptable.to()==pathAlreadyOriented.from() ||
			   pathAdaptable.to()==pathAlreadyOriented.to()) : pathAdaptable.from() + " -> " + pathAdaptable.to() + " ET " + pathAlreadyOriented.from() + " -> " + pathAlreadyOriented.to();
		
		// on met les 2 path dans le meme sens si le Side de leur contrainte geom est identique,
		//  tête bèche sinon
		if (pathAdaptable.getContrainteGeom().sideDissymetrie == pathAlreadyOriented.getContrainteGeom().sideDissymetrie) {
			if (pathAdaptable.to() == pathAlreadyOriented.to() || pathAdaptable.from() == pathAlreadyOriented.from()) {
				pathAdaptable.reverse();
			}
		} else {
			if (pathAdaptable.to() == pathAlreadyOriented.from() || pathAdaptable.from() == pathAlreadyOriented.to()) {
				pathAdaptable.reverse();
			}
		}
		pathAdaptable.bOrientationSetted = true;
	}


	//**************************************************************************
	// Divers
	//**************************************************************************


	// A partir d'un vecteur de Path, renvoie uniquement ceux qui ont une dissymétrie et non encore orienté
	private static ArrayList<PathREF> getPathWithDissymetrieAndNotYetOriented(ArrayList<PathREF> vPath) {
		ArrayList<PathREF> vRes = new ArrayList<PathREF>();
		for (PathREF p : vPath)
			if (p.hasDissymetrie() && !p.bOrientationSetted)
				vRes.add(p);
		return vRes;
	}


	// Un path a une orientation contrainte s'il y a decallage suffisant entre
	//  le nombre d'intersection de barbules gauches et droites en absolu
	//  et relativement au nombre total de barbule traitées
	private static boolean hasOrientationContrainte(A_Path path) {
		int nbTotal = path.nbTotal_barbule;
		int nbD = path.nbIntersect_barbuleDroite;
		int nbG = path.nbIntersect_barbuleGauche;

		if (nbD==0 && nbG==0)
			return false;

		int ecartGD = Math.max(nbD, nbG) - Math.min(nbD, nbG);
		if (ecartGD <= 1 || ((double)ecartGD)/nbTotal < 0.1)
			// ecart G/D insignifiant
			return false;

		return true;
	}


	// Parmi les path connectés à j, renvoit un qui n'est pas encore orienté
	//  et différent du 'path' fourni en argument
	//  si plusieurs choix, on priviligiera un path de même contrainte geom
	// renvoie null si aucun path non orienté
	private static A_Path getOnePathNotOrientedConnectedTo(A_Path path, A_Junction j) {
		A_Path pathTrouve = null;

		int nbPath = j.getNbPathContenant();
		for (int i=0; i<nbPath ; i++) {
			A_Path path2 = j.getPathContenantNum(i);
			if (path2 == path) continue;
			if (!path2.hasCommonExtremityWith(path)) continue;

			if (!path2.bOrientationSetted) {
				if (pathTrouve==null)
					// si c'est le 1er, on le prend
					pathTrouve = path2;
				else if (path2.getContrainteGeom()==path.getContrainteGeom())
					// si c'est un autre, on remplace que si la contrainte geom est identique à 'path'
					pathTrouve = path2;
			}
		}
		return pathTrouve;
	}



}
